/*
 * Copyright (c) 2021 The red-star Project
 *
 * Licensed under the Apache License, version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.inyourcode.example.service.handler;

import com.alibaba.fastjson.JSONObject;
import com.inyourcode.core.cluster.ClusterNodeManager;
import com.inyourcode.core.cluster.api.ClusterConst;
import com.inyourcode.core.util.JWTUtil;
import com.inyourcode.core.db.BasicDataContext;
import com.inyourcode.core.db.EntityWrapper;
import com.inyourcode.core.db.api.IDataClassHolder;
import com.inyourcode.core.db.iml.MongoResource;
import com.inyourcode.core.db.iml.MutilpleResource;
import com.inyourcode.core.db.iml.RedisResource;
import com.inyourcode.core.db.redis.RedisConfig;
import com.inyourcode.core.event.EventSystem;
import com.inyourcode.core.threads.api.HashExecutor;
import com.inyourcode.core.transport.session.BasicSession;
import com.inyourcode.core.transport.session.ResponseContext;
import com.inyourcode.core.transport.session.api.MessageHolder;
import com.inyourcode.core.transport.session.RequestContext;
import com.inyourcode.core.transport.session.SessionService;
import com.inyourcode.core.transport.session.api.NoAuthRequest;
import com.inyourcode.core.transport.session.api.RequestMapping;
import com.inyourcode.core.transport.session.api.Session;
import com.inyourcode.example.cluster.lobby1.LobbyNode;
import com.inyourcode.example.service.IErrorCode;
import com.inyourcode.example.service.InvokeId;
import com.inyourcode.example.service.RedisScripter;
import com.inyourcode.example.service.data.C2LLogin;
import com.inyourcode.example.service.data.C2Logout;
import com.inyourcode.example.service.data.L2CLogin;
import com.inyourcode.example.service.data.L2CLogout;
import com.inyourcode.example.service.data.MSGBuilder;
import com.inyourcode.example.service.data.player.PlayerBaseInfo;
import com.inyourcode.example.service.data.player.PlayerItems;
import com.inyourcode.core.threads.ConsumerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Controller;

import java.util.List;

/**
 * https://metrics.dropwizard.io/3.1.0/manual/core/#other-reporters
 * @author JackLei
 */
@Controller
public class LoginHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(LoginHandler.class);
    @Value("${redis.cache.expire}")
    private int redisCacheExpire;
    @Autowired
    JWTUtil jwtUtil;
    @Autowired
    @Qualifier(RedisConfig.STRING_REDIS_TEMPLATE)
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    @Qualifier(RedisConfig.BYTES_REDIS_TEMPLATE)
    RedisTemplate<String, byte[]> bytesRedisTemplate;
    @Autowired
    MongoTemplate mongoTemplate;
    @Autowired
    ClusterNodeManager clusterNodeManager;
    @Autowired
    IDataClassHolder dataClassHolder;
    @Autowired
    RedisScripter redisScripter;
    @Autowired
    @Qualifier(LobbyNode.BEAN_NAME_DB_EXECUTOR)
    HashExecutor dbTaskExecutor;

    @NoAuthRequest
    @RequestMapping(builder = C2LLogin.class, invokeId = InvokeId.C2L_Login)
    public ResponseContext login(Session session, RequestContext requestContext){
        C2LLogin msg = requestContext.getData();
        L2CLogin result = new L2CLogin();

        LOGGER.info("login step0 , msg:{}", JSONObject.toJSONString(msg));

        JWTUtil.JWTResult jwtResult = jwtUtil.parse(msg.token);
        LOGGER.info("login step1 , jwtResult:{}", JSONObject.toJSONString(jwtResult));
        String jwtOpenId = jwtResult.getUid();
        if (!jwtOpenId.equals(msg.openId)) {
            LOGGER.error("Parameter mismatch, uid:{}, openId:{}", jwtOpenId, msg.openId);
            result.code = IErrorCode.LOGIN_JWT_AUTH_ERROR;
            return ResponseContext.create(InvokeId.L2C_Login, result);
        }

        JSONObject jwtJSON = JSONObject.parseObject(jwtResult.getExtendData());
        String jwtPlayerId = jwtJSON.getString("playerId");

        ValueOperations opsForValue = stringRedisTemplate.opsForValue();
        String playerIdFromRedis = (String) opsForValue.get(ClusterConst.KEY_PLAYER_ID_MAPPING + jwtOpenId);
        if (!playerIdFromRedis.equals(jwtPlayerId)) {
            LOGGER.error("Parameter mismatch, playerIdFromRedis:{}, jwtPlayerId:{}", playerIdFromRedis, jwtPlayerId);
            result.code = IErrorCode.LOGIN_JWT_AUTH_ERROR;
            return ResponseContext.create(InvokeId.L2C_Login, result);
        }


        String nodeIdFromRedis = (String) opsForValue.get(ClusterConst.KEY_PLAYER_BIND_SERVER + jwtOpenId);
        String currentNodeId = clusterNodeManager.getCurrentClusterNodeConf().uniqueKey();
        if (!nodeIdFromRedis.equals(currentNodeId)) {
            result.code = IErrorCode.LOGIN_RESET;
            LOGGER.error("Parameter mismatch, nodeIdFromRedis:{}, currentNodeId:{}", nodeIdFromRedis, currentNodeId);
            return ResponseContext.create(InvokeId.L2C_Login, result);
        }

        Long playerId = Long.valueOf(jwtPlayerId);
        Session oldSession = SessionService.getSessionByUid(playerId);
        if (oldSession != null) {
            result.code = IErrorCode.LOGIN_REPLACE_ACCOUNT;
            oldSession.closeAndNotify(new ResponseContext(InvokeId.L2C_Login, requestContext.getTraceId(), result, requestContext.getSerializerCode()));

            oldSession.replaceInfo(session);
            session = oldSession;
            SessionService.update(session.channel(), session);
        } else {
            session.setId(playerId);
            session.setAttribute(BasicSession.ATTR_OPENID, jwtOpenId);
            List<EntityWrapper> wrapperList = dataClassHolder.wrap();
            MutilpleResource mutilpleResource = new MutilpleResource(new RedisResource(bytesRedisTemplate), new MongoResource(mongoTemplate, "player"));
            session.setDataContext(new BasicDataContext(playerId, mutilpleResource, wrapperList));
            SessionService.create(playerId, session);
        }

        session.setAuth(true);
        session.loadAllData(dbTaskExecutor, new ConsumerTask(session) {
            @Override
            public void accept(Exception e) {
                Session attachSession = (Session) attach;
                EventSystem.emit("login", attachSession);

                result.playerInfo = MSGBuilder.build(attachSession.getContextData(PlayerBaseInfo.class));
                result.playerItem = MSGBuilder.build(attachSession.getContextData(PlayerItems.class));

                ResponseContext responseContext = ResponseContext.create(InvokeId.L2C_Login, requestContext.getTraceId(), result, requestContext.getSerializerCode());
                attachSession.write(responseContext);
            }
        });

        return null;
    }

    @NoAuthRequest
    @RequestMapping(builder = L2CLogin.class, invokeId = InvokeId.L2C_Login)
    public ResponseContext respLogin(Session session, RequestContext requestContext){
        L2CLogin msg = requestContext.getData();
        LOGGER.info("player login:{}", JSONObject.toJSONString(msg));
        return null;
    }

    @RequestMapping(builder = C2Logout.class, invokeId = InvokeId.C2L_Logout)
    public ResponseContext logOut(Session session, RequestContext requestContext){
        LOGGER.info("player logout:{}", session.getId());
        session.saveAllData(dbTaskExecutor, new ConsumerTask(session) {
            @Override
            public void accept(Exception exception) {
                if (exception == null) {
                    LOGGER.info("The player has exited, playerId:{}", session.getId());
                }
            }

            @Override
            public void asyncAfterHandle() {
                Session session = (Session)this.attach;

                String openId = session.getAttribute(BasicSession.ATTR_OPENID).toString();
                String clusterBindKey = ClusterConst.KEY_PLAYER_BIND_SERVER + openId;
                String uniqueKey = clusterNodeManager.getCurrentClusterNodeConf().uniqueKey();

                boolean cas = redisScripter.cas(clusterBindKey, uniqueKey, "");
                SessionService.remove(session.channel());
                SessionService.remove(session.getId());

                if (cas) {
                    LOGGER.info("Release cluster binding successed, playerId:{}, oldNode:{}", session.getId(), uniqueKey);
                    session.cacheDataOptExpire(true, redisCacheExpire);
                } else {
                    LOGGER.error("Release cluster binding failure, playerId:{}, oldNode:{}", session.getId(), uniqueKey);
                }
            }
        });

        L2CLogout resp = new L2CLogout();
        return ResponseContext.create(InvokeId.L2C_Login ,resp);
    }

    @RequestMapping(builder = L2CLogout.class, invokeId = InvokeId.L2C_Logout)
    public MessageHolder respLogout(Session session, RequestContext requestContext){
        L2CLogout msg = requestContext.getData();
        LOGGER.info("player loglout:{}", JSONObject.toJSONString(msg));
        return null;
    }
}
